/*
  AdViEmulator - AdventureVision emulator
  Copyright (C) 2012-2013  JustBurn

  This program is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "adviemulator.h"

#define COLUMNSSTROBE_SYNC	38
#define COLUMNSSTROBE_START	54
#define COLUMNSSTROBE_END	354
#define COLUMNSSTROBE_FULL	384
#define COLUMNSSTROBE_MAX	2222

#define LATCHSTROBE_START	42
#define LATCHSTROBE_CCOL	22
#define LATCHSTROBE_CSENS	352
#define LATCHSTROBE_CDISP	48532

AdViEmulator::AdViEmulator(float samplerate, int buffsamples)
	: cpu(this), apu(this)
{
	state = STATE_STOP;
	soundBuffSize = buffsamples;
	soundPreset = (int)((733333.0f / samplerate) * 16777216.0f);
	cntAPUPreset = 52;
	enableBreaks = true;
	memset(BIOS, 0, 1024);
	memset(ROM, 0, 4096);
	memset(IRAM, 0, 64);
	memset(XRAM, 0, 1024);
	memset(BIOS_Trap, 0, 1024);
	memset(ROM_Trap, 0, 4096);
	memset(IRAM_Trap, 0, 64);
	memset(XRAM_Trap, 0, 1024);
	videoColumnMode = false;
	videoAnalogDisplay = false;
	analogDecay = 85;
	cycCnt0 = 0;
	keyInput = 0;
	avInput = 0xFF;
	Reset();
	SetVolume(64);
}

AdViEmulator::~AdViEmulator()
{
}

void AdViEmulator::Reset(void)
{
	p1 = 0xFF;
	p2 = 0xFF;
	p2m = p2 & 0xF0;
	soundCounter = soundPreset;
	cntVidPremod = LATCHSTROBE_CDISP;
	cntVidValue = 0;
	cntAnalogPremod = 24000;
	cntAnalogValue = 0;
	cntAPUValue = 0;
	breakExecution = false;
	breakReason = BREAK_POINT;
	breakException[0] = 0;
	traceCPUPtr = 0;
	memset(traceCPUType, NON_VALID, sizeof(traceCPUType));
	memset(traceCPUAddr, 0xFF, sizeof(traceCPUAddr));
	traceAPUPtr = 0;
	memset(traceAPUAddr, 0xFF, sizeof(traceAPUAddr));
	APURunningLast = APURunning = true;
	memset(displayPre, 0xFF, 5);
	memset(displayPost, 0xFF, 5);
	columnsStrobe = 0;
	memset(videoOut, 0xFF, sizeof(videoOut));
	memset(videoOutAD, 0, sizeof(videoOutAD));
	cpu.Reset();
	apu.Reset();
	apu.port_l = 0;
}

uint8_t AdViEmulator::ReadROMData(uint16_t addr)
{
	if (IsBIOS(addr)) {
		return BIOS[addr & 1023];
	} else {
		return ROM[addr & 4095];
	}
}

int AdViEmulator::ReadROMType(uint16_t addr)
{
	if (IsBIOS(addr)) {
		return CPU_BIOS;
	} else {
		return CPU_ROM;
	}
}

bool AdViEmulator::IsBIOS(uint16_t addr)
{
	return (addr < 1024) && !(p1 & 4);
}

uint8_t AdViEmulator::GetP1(void)
{
	return p1 & avInput;
}

uint8_t AdViEmulator::GetP2(void)
{
	return p2;
}

uint8_t CPU8048::OnDecodeROM(uint16_t addr)
{
	if (avemu->IsBIOS(addr)) {
		return avemu->BIOS[addr & 1023];
	} else {
		return avemu->ROM[addr & 4095];
	}
}

uint8_t CPU8048::OnReadROM(uint16_t addr)
{
	if (avemu->IsBIOS(addr)) {
		addr &= 1023;
		if (avemu->enableBreaks && (avemu->BIOS_Trap[addr] & AdViEmulator::TRAP_READ)) avemu->breakExecution = true;
		return avemu->BIOS[addr];
	} else {
		addr &= 4095;
		if (avemu->enableBreaks && (avemu->ROM_Trap[addr] & AdViEmulator::TRAP_READ)) avemu->breakExecution = true;
		return avemu->ROM[addr];
	}
}

inline void AdViEmulator::HardwarePoke(int faddr)
{
	switch (p2 & 0xE0) {
	case 0x20: displayPre[0] = XRAM[faddr]; break;
	case 0x40: displayPre[1] = XRAM[faddr]; break;
	case 0x60: displayPre[2] = XRAM[faddr]; break;
	case 0x80: displayPre[3] = XRAM[faddr]; break;
	case 0xA0: displayPre[4] = XRAM[faddr]; break;
	case 0xC0: APURunning = XRAM[faddr] & 1; break;
	}
}

uint8_t CPU8048::OnReadRAM(uint8_t addr)
{
	int faddr = addr & 63;
	if (avemu->enableBreaks && (avemu->IRAM_Trap[faddr] & AdViEmulator::TRAP_READ)) avemu->breakExecution = true;
	return avemu->IRAM[faddr];
}

void CPU8048::OnWriteRAM(uint8_t addr, uint8_t data)
{
	int faddr = addr & 63;
	if (avemu->enableBreaks && (avemu->IRAM_Trap[faddr] & AdViEmulator::TRAP_WRITE)) avemu->breakExecution = true;
	avemu->IRAM[faddr] = data;
}

uint8_t CPU8048::OnReadEx(uint8_t addr)
{
	int faddr = (((int)avemu->p1 << 8) & 0x300) | addr;
	if (avemu->enableBreaks && (avemu->XRAM_Trap[faddr] & AdViEmulator::TRAP_READ)) avemu->breakExecution = true;
	avemu->HardwarePoke(faddr);
	return avemu->XRAM[faddr];
}

void CPU8048::OnWriteEx(uint8_t addr, uint8_t data)
{
	int faddr = (((int)avemu->p1 << 8) & 0x300) | addr;
	if (avemu->enableBreaks && (avemu->XRAM_Trap[faddr] & AdViEmulator::TRAP_WRITE)) avemu->breakExecution = true;
	avemu->XRAM[faddr] = data;
	avemu->HardwarePoke(faddr);
}

uint8_t CPU8048::OnPortRd(uint8_t port)
{
	switch (port) {
	case 1:	// P1
		return avemu->p1 & avemu->avInput;
	case 2:	// P2
		return avemu->p2;
	default: // BUS, P4~7
		return 0xFF;
	};
}

void CPU8048::OnPortWr(uint8_t port, uint8_t data)
{
	uint8_t mdata;
	switch (port) {
	case 1:	// P1
		avemu->p1 = data;
		break;
	case 2:	// P2
		mdata = data & 0xF0;
		if (avemu->videoColumnMode) {
			if ((mdata == 0x10) && (avemu->p2m != 0x10)) {
				// Latch (0x10)
				memcpy(avemu->displayPost, avemu->displayPre, 5);

			}
			if ((mdata == 0x00) && (avemu->p2m != 0x00)) {
				// Clear (0x00)
				memset(avemu->displayPre, 0xFF, 5);
				memset(avemu->displayPost, 0xFF, 5);
			}
		} else {
			if ((mdata == 0x10) && (avemu->p2m != 0x10)) {
				// Latch (0x10)
				memcpy(avemu->displayPost, avemu->displayPre, 5);
				if (avemu->columnsStrobe < COLUMNSSTROBE_END) {
					avemu->VideoLatch();
					avemu->columnsStrobe++;
					avemu->VideoLatch();
					avemu->columnsStrobe++;
				} else avemu->columnsStrobe += 2;
			}
			// Clear not available in old method
		}
		avemu->p2 = data;
		avemu->p2m = mdata;
		avemu->apu.port_l = data >> 4;
		break;
	default: // BUS, P4~7
		break;
	};
}

void CPU8048::OnException(CPU8048::Exception exception, int value)
{
	avemu->breakExecution = true;
	avemu->breakReason = AdViEmulator::BREAK_EXCEPTION;
	switch (exception) {
	case INVALID_INSTRUCTION:
		sprintf(avemu->breakException, "Invalid CPU instruction %02X", value);
		break;
	}
}

void APUCOP411::OnException(APUCOP411::Exception exception, int value)
{
	avemu->breakExecution = true;
	avemu->breakReason = AdViEmulator::BREAK_EXCEPTION;
	switch (exception) {
	case INVALID_INSTRUCTION:
		sprintf(avemu->breakException, "Invalid COP411 instruction %02X", value);
		break;
	}
}

inline void AdViEmulator::AddCPUTrace()
{
	traceCPUType[traceCPUPtr] = IsBIOS(cpu.pc.w) ? CPU_BIOS : CPU_ROM;
	traceCPUAddr[traceCPUPtr++] = cpu.pc.w;
}

inline void AdViEmulator::AddAPUTrace()
{
	traceAPUAddr[traceAPUPtr++] = apu.pc.w;
}

inline void AdViEmulator::AnalogDecay(void)
{
	if (!IsRunning()) return;
	for (int i=0; i<384*40; i++) {
		int decay = videoOutAD[i] - analogDecay;
		if (decay < 0) decay = 0;
		videoOutAD[i] = decay;
	}
}

inline void AdViEmulator::HardwareProc(int cyc)
{
	// For profiling
	cycCnt0 += cyc;

	// For analog decay
	cntAnalogValue += cyc;
	if (cntAnalogValue >= cntAnalogPremod) {
		cntAnalogValue = 0;
		if (videoAnalogDisplay) AnalogDecay();
	}

	// For video syncronization
	cntVidValue += cyc;
	if (cntVidValue >= cntVidPremod) {
		if (videoColumnMode) {
			cntVidValue -= cntVidPremod;
			cntVidPremod = LATCHSTROBE_CCOL;
			if (columnsStrobe < COLUMNSSTROBE_SYNC) {
				VideoLatch();
				columnsStrobe++;
			} else if (columnsStrobe < COLUMNSSTROBE_START) {
				cpu.gio_t1 = 0;
				VideoLatch();
				columnsStrobe++;
			} else if (columnsStrobe < COLUMNSSTROBE_FULL) {
				cpu.gio_t1 = 1;
				VideoLatch();
				columnsStrobe++;
			} else if (columnsStrobe < COLUMNSSTROBE_MAX) {
				columnsStrobe++;
			} else {
				columnsStrobe = 0;
				cntAnalogValue = 0;
			}
		} else {
			cntVidValue -= cntVidPremod;
			cpu.gio_t1 = !cpu.gio_t1;
			if (cpu.gio_t1) {
				cntVidPremod = LATCHSTROBE_CDISP;
			} else {
				cntVidPremod = LATCHSTROBE_CSENS;
				columnsStrobe = LATCHSTROBE_START;
				cntAnalogValue = 0;
			}
		}
	}

	// For audio microcontroller
	cntAPUValue += cyc << 2;
	if (APURunning) {
		if (cntAPUValue >= cntAPUPreset) {
			AddAPUTrace();
			cntAPUValue -= cntAPUPreset * apu.ExecSingle();
			APURunningLast = true;
		}
	} else {
		if (APURunningLast) apu.Reset();
		cntAPUValue = -cntAPUPreset * 2;
		APURunningLast = false;
	}
}

void AdViEmulator::VideoLatch()
{
	memcpy(videoOut + columnsStrobe * 5, displayPost, 5);
	if (this->videoAnalogDisplay) {
		uint8_t *ptr = videoOutAD + columnsStrobe * 40;
		for (int y=0; y<5; y++) {
			for (int b=0; b<8; b++) {
				if (~displayPost[y] & (1 << b)) ptr[y*8+b] = 255;
			}
		}
	}
}

int AdViEmulator::Emulate(bool sound, int mincycles, int minsndsamples)
{
	int inicycles = mincycles;
	breakExecution = false;
	if (sound) {
		minsndsamples -= NumSamples();
		while (!breakExecution && ((mincycles > 0) || (minsndsamples > 0))) {
			AddCPUTrace();
			int cyc = cpu.ExecSingle();
			HardwareProc(cyc);
			soundCounter -= cyc << 24;
			if (soundCounter < 0) {
				uint8_t samp = 0x80;
				if (apu.port_g & 1) {
					if (apu.port_d & 1) samp = soundVolume2;
					else samp = soundVolume1;
				}
				AddSample(samp);
				minsndsamples--;
				soundCounter += soundPreset;
			}
			mincycles -= cyc;
		}
	} else {
		while (!breakExecution && (mincycles > 0)) {
			AddCPUTrace();
			int cyc = cpu.ExecSingle();
			HardwareProc(cyc);
			mincycles -= cyc;
		}
	}
	return inicycles - mincycles;
}

void AdViEmulator::CPUSingleStep()
{
	AddCPUTrace();
	int cyc = cpu.ExecSingle();
	HardwareProc(cyc);
}

void AdViEmulator::APUSingleStep()
{
	int curapuval;
	do {
		curapuval = cntAPUValue;
		AddCPUTrace();
		int cyc = cpu.ExecSingle();
		HardwareProc(cyc);
	} while(curapuval < cntAPUValue);
}

void AdViEmulator::SetVideoMode(bool column, bool analog, int analogdecay)
{
	if ((this->videoColumnMode != column) || (this->videoAnalogDisplay != analog) || (this->analogDecay != analogdecay)) {
		memset(videoOut, 0xFF, sizeof(videoOut));
		memset(videoOutAD, 0, sizeof(videoOutAD));
		this->videoAnalogDisplay = analog;
		this->videoColumnMode = column;
		this->analogDecay = analogdecay;
	}
}

void AdViEmulator::SetState(bool run)
{
	this->state = run ? STATE_RUN : STATE_STOP;
}

int AdViEmulator::IsRunning()
{
	return this->state == STATE_RUN;
}

void AdViEmulator::SuspendState()
{
	if (state == STATE_RUN) state = STATE_HALT;
}

void AdViEmulator::ResumeState()
{
	if (state == STATE_HALT) state = STATE_RUN;
}

uint16_t AdViEmulator::MemSize(int type)
{
	switch (type) {
	case CPU_BIOS: return 1024;
	case CPU_IRAM: return 64;
	case CPU_ROM: return 4096;
	case CPU_XRAM: return 1024;
	case APU_IROM: return 512;
	case APU_IRAM: return 32;
	}
	return 0;
}

uint8_t AdViEmulator::MemRead(int type, uint16_t addr)
{
	switch (type) {
	case CPU_BIOS: return BIOS[addr & 1023];
	case CPU_IRAM: return IRAM[addr & 63];
	case CPU_ROM: return ROM[addr & 4095];
	case CPU_XRAM: return XRAM[addr & 1023];
	case APU_IROM: return apu.rom[addr & 511];
	case APU_IRAM: return apu.ram[addr & 31];
	}
	return 0;
}

void AdViEmulator::MemWrite(int type, uint16_t addr, uint8_t data)
{
	switch (type) {
	case CPU_BIOS: BIOS[addr & 1023] = data; break;
	case CPU_IRAM: IRAM[addr & 63] = data; break;
	case CPU_ROM: ROM[addr & 4095] = data; break;
	case CPU_XRAM: XRAM[addr & 1023] = data; break;
	case APU_IROM: apu.rom[addr & 511] = data; break;
	case APU_IRAM: apu.ram[addr & 31] = data & 15; break;
	}
}

void AdViEmulator::ClearAllBreaks(void)
{
	memset(BIOS_Trap, 0, 1024);
	memset(ROM_Trap, 0, 4096);
	memset(IRAM_Trap, 0, 64);
	memset(XRAM_Trap, 0, 1024);
}

bool AdViEmulator::SetBreak(int type, int addr, int traps)
{
	switch (type) {
	case CPU_BIOS: BIOS_Trap[addr & 1023] = traps; return true;
	case CPU_IRAM: IRAM_Trap[addr & 63] = traps; return true;
	case CPU_ROM: ROM_Trap[addr & 4095] = traps; return true;
	case CPU_XRAM: XRAM_Trap[addr & 1023] = traps; return true;
	case APU_IROM: return false;
	case APU_IRAM: return false;
	default: return false;
	}
	return false;
}

int AdViEmulator::GetBreak(int type, int addr)
{
	switch (type) {
	case CPU_BIOS: return BIOS_Trap[addr & 1023];
	case CPU_IRAM: return IRAM_Trap[addr & 63];
	case CPU_ROM: return ROM_Trap[addr & 4095];
	case CPU_XRAM: return XRAM_Trap[addr & 1023];
	case APU_IROM: return 0;
	case APU_IRAM: return 0;
	}
	return 0;
}

void AdViEmulator::GetCPUTrace(int offset, uint8_t *type, uint16_t *addr)
{
	int vaddr = (traceCPUPtr - offset - 1) & 255;
	if (type) *type = traceCPUType[vaddr];
	if (addr) *addr = traceCPUAddr[vaddr];
}

void AdViEmulator::GetAPUTrace(int offset, uint16_t *addr)
{
	int vaddr = (traceAPUPtr - offset - 1) & 255;
	if (addr) *addr = traceAPUAddr[vaddr];
}

int AdViEmulator::NumSamples()
{
	return soundFIFO.size();
}

uint8_t AdViEmulator::GetSample()
{
	if (!soundFIFO.size()) return 0x80;
	uint8_t samp = soundFIFO.front();
	soundFIFO.pop();
	return samp;
}

int AdViEmulator::GetSamples(uint8_t *buff, int samples)
{
	for (int i=0; i<samples; i++) buff[i] = GetSample();
	return samples;
}

void AdViEmulator::AddSample(uint8_t sample)
{
	if (soundFIFO.size() >= (size_t)soundBuffSize) return;
	soundFIFO.push(sample);
}

void AdViEmulator::SetVolume(uint8_t vol)
{
	if (vol > 127) vol = 127;
	soundVolume1 = 0x80 | vol;
	soundVolume2 = 0x80 | (vol >> 2);
}

uint8_t AdViEmulator::GetVolume()
{
	return soundVolume1 & 127;
}

void AdViEmulator::SetAPUFreq(int freq)
{
	cntAPUPreset = 2933332 / freq;
}

void AdViEmulator::SetInput(uint8_t input, bool state)
{
	if (state) {
		if ((input >= 4) && (keyInput & 0xF0))
			keyInput = (keyInput & 0x0F) | (1 << input);
		else
			keyInput |= (1 << input);
	} else
		keyInput &= ~(1 << input);
	uint8_t inpVa = ~keyInput | 0x07;
	if (keyInput & (1 << BUTTON_1)) inpVa &= 0xCF;
	if (keyInput & (1 << BUTTON_2)) inpVa &= 0xAF;
	if (keyInput & (1 << BUTTON_4)) inpVa &= 0x6F;
	avInput = inpVa | 0x07;
}
